// TODO: CODE REVIEW & CLEANUP!
//
// ClientEndpoint.cs
//
// Implements ClientEndpoint and related types.
//

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Windows.Threading;
using System.Xml;
using HomeUX.Utilities;

namespace HomeUX.Networking
{

// manages the client side of a connection to a server that implements the MessageEndpoint protocol
public class ClientEndpoint : MessageEndpoint, IMessageEndpoint, IDisposable
{
    object _lock = new object();

    DispatcherTimer _pollTimer;
	string _serverHostName;
	int _serverPortNumber;
    Guid _sessionId;
    int _receiveBufferSize = 50000;
    Socket _socket;

    byte[] _receiveBuffer;

    DateTime _lastPingTime;

    Status<ClientEndpointStatus> _status = new Status<ClientEndpointStatus>();

    Queue<Message> _incomingMessages = new Queue<Message>();

    // the status as of this moment -- see also ReportedStatus
    public ClientEndpointStatus ImmediateStatus
    {
        get
        {
            lock (_lock)
                return _status.Value;
        }
    }

    ClientEndpointStatus _reportedStatus;

	// the status as last reported by the StatusChanged event -- see also ImmediateStatus
    public ClientEndpointStatus ReportedStatus
    {
        get
        {
            return _reportedStatus;
        }
        set
        {
            bool wasConnected = IsConnected;
            _reportedStatus = value;
            if ((wasConnected != IsConnected) && (IsConnectedChanged != null))
                IsConnectedChanged(this, EventArgs.Empty);
        }
    }

    // is based on ReportedStatus, so if IsConnected is true it's still possible that a subsequent
    // call to SendMessage will return false, indicating that the connection was lost between the
    // time of the last report and the SendMessage call
    public bool IsConnected
    {
        get
        {
            return ReportedStatus == ClientEndpointStatus.Connected;
        }
    }

    public event EventHandler IsConnectedChanged;

    public bool IsConnectionClosed
    {
        get
        {
            lock (_lock)
                return (_socket == null);
        }
    }

	// this event is fired on the UI thread
    public event EventHandler<ClientEndpointStatusChangedEventArgs> StatusChanged;

	// this event is fired on the UI thread
    public event EventHandler<MessageReceivedEventArgs> MessageReceived;

    public string ServerHostName
    {
        get
        {
            lock (_lock)
                return _serverHostName;
        }
        set
        {
            lock (_lock)
            {
                DisallowPropertySetAfterConnectToServer("ServerHostName");
                _serverHostName = value;
            }
        }
    }

    public int ServerPortNumber
    {
        get
        {
            lock (_lock)
                return _serverPortNumber;
        }
        set
        {
            lock (_lock)
            {
                DisallowPropertySetAfterConnectToServer("ServerPortNumber");
                _serverPortNumber = value;
            }
        }
    }

    public new string Password
    {
        get
        {
            lock (_lock)
                return base.Password;
        }
        set
        {
            lock (_lock)
            {
                DisallowPropertySetAfterConnectToServer("Password");
                base.Password = value;
            }
        }
    }

    public Guid SessionId
    {
        get
        {
            lock (_lock)
                return _sessionId;
        }
        set
        {
            lock (_lock)
            {
                DisallowPropertySetAfterConnectToServer("SessionId");
                _sessionId = value;
            }
        }
    }

    public int ReceiveBufferSize
    {
        get
        {
            lock (_lock)
                return _receiveBufferSize;
        }
        set
        {
            lock (_lock)
            {
                DisallowPropertySetAfterConnectToServer("ReceiveBufferSize");
                _receiveBufferSize = value;
            }
        }
    }

    void DisallowPropertySetAfterConnectToServer(string propertyName)
    {
        lock (_lock)
        {
            if (_socket != null)
            {
                throw new InvalidOperationException(String.Format(
                    "Property \"{0}\" may only be set before ConnectToServer has been called",
                    propertyName));
            }
        }
    }

    static InvalidOperationException NewPropertyNotSetException(string propertyName)
    {
        throw new InvalidOperationException(String.Format(
            "Property \"{0}\" must be set before calling ConnectToServer", propertyName));
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
	public ClientEndpoint()
	{
		_pollTimer = new DispatcherTimer();
		_pollTimer.Tick += Poll;
	}

    /// <summary>
    /// TODO
    /// </summary>
    ///
    public new void ConnectToServer()
	{
        lock (_lock)
        {
            if (_serverHostName == null)
                throw NewPropertyNotSetException("ServerHostName");
            if (_serverPortNumber == 0)
                throw NewPropertyNotSetException("ServerPortNumber");
            if (_sessionId == Guid.Empty)
                _sessionId = Guid.NewGuid();

            _receiveBuffer = new byte[_receiveBufferSize];

            // if we're currently connecting or already connected, return, otherwise update
			// <_status> to indicate that connection is underway
			if (_socket != null)
                return;
            else
                _status.Value = ClientEndpointStatus.Connecting;

            var dnsEndPoint = new DnsEndPoint(_serverHostName, _serverPortNumber, AddressFamily.InterNetwork);
            _socket = new Socket(dnsEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

            // we poll because Silverlight doesn't allow BeginInvoke to be called from the
            // socket thread (at least in some cases)
            _pollTimer.Interval = TimeSpan.FromMilliseconds(50);
            _pollTimer.Start();

            // begin the process of attempting to connect to the server
            var connectArgs = new SocketAsyncEventArgs();
            connectArgs.RemoteEndPoint = dnsEndPoint;
            connectArgs.Completed += OnConnectAttemptEnded;
            if (!_socket.ConnectAsync(connectArgs))
                OnConnectAttemptEnded(_socket, connectArgs);
        }
	}

    /// <summary>
    /// Called when the attempt to connect to the server has ended, either successfully or
    /// unsuccessfully.
    /// </summary>
    ///
    /// <param name="sender">The event sender.</param>
    ///
    /// <param name="e">Event arguments.</param>
    ///
    void OnConnectAttemptEnded(object sender, SocketAsyncEventArgs e)
	{
        // NOTE: THIS METHOD MAY RUN ON A BACKGROUND THREAD
        lock (_lock)
        {
            try
            {
                if (e.SocketError != SocketError.Success)
                {
                    CloseConnection(ClientEndpointStatus.FailedToConnect);
                    return;
                }

                // update status: we're now in the handshake phase
                _status.Value = ClientEndpointStatus.Negotiating;

                // prepare <_socket> to receive the next buffer of data
                BeginReceiveNext();

                // tell the base class to perform handshaking
                base.ConnectToServer();
            }
            catch (Exception ex)
            {
                // @NOTE: Can't do "Application.Current.RootVisual.Dispatcher.BeginInvoke(
                // delegate { throw new SecondaryThreadException(ex); })" because access to
                // RootVisual is denied from this thread.  But if the exception isn't caught
                // here, VS will refuse to catch it in the running debugger -- it will launch
                // a new debugging session, which fails since there's an attached debugger.
                // So, I call Debugger.Break().
                Ignore(ex);
                Debugger.Break();
                throw; // terminate this thread for security reasons
            }
        }
    }

    // tell <_socket> to call OnReceiveAttemptEnded when the next data is received
    /// <summary>
    /// TODO
    /// </summary>
    ///
    void BeginReceiveNext()
    {
        // assert: inside lock(_lock)
        var receiveArgs = new SocketAsyncEventArgs();
        receiveArgs.SetBuffer(_receiveBuffer, 0, _receiveBuffer.Length);
        receiveArgs.Completed += OnReceiveAttemptEnded;
        if (!_socket.ReceiveAsync(receiveArgs))
            OnReceiveAttemptEnded(_socket, receiveArgs);
    }

    /// <summary>
    /// Called when an attempt to receive data from the socket has ended, either successfully or
    /// unsuccessfully.
    /// </summary>
    ///
    /// <param name="sender">The event sender.</param>
    ///
    /// <param name="e">Event arguments.</param>
    ///
    void OnReceiveAttemptEnded(object sender, SocketAsyncEventArgs e)
    {
        // NOTE: THIS METHOD RUNS ON A BACKGROUND THREAD
        lock (_lock)
        {
            try
            {
                if (e.SocketError != SocketError.Success)
                {
                    CloseConnection(ClientEndpointStatus.LostConnection);
                    return;
                }

                // give the received data to the base class, which parses it into messages and
                // implements handshaking etc.
                OnDataReceived(e.Buffer, e.BytesTransferred);

                // set up <_socket> to receive the next data from the server
                BeginReceiveNext();
            }
            catch (IncorrectPasswordException)
            {
                CloseConnection(ClientEndpointStatus.PasswordIncorrect);
                return;
            }
#if !NOCATCH
            catch (Exception ex)
            {
                if (!(ex is System.Threading.ThreadAbortException))
                {
					// see @NOTE in this file
					Ignore(ex);
					Debugger.Break();
					throw; // terminate this thread for security reasons
                }
            }
#endif
        }
    }

    // return false rather than throwing an exception because server could disconnect between the
    // time that the caller checks Status and the caller calls SendMessage
    /// <summary>
    /// TODO
    /// </summary>
    ///
    /// <param name="message">TODO</param>
    ///
    public new bool SendMessage(Message message)
    {
        lock (_lock)
        {
            if (_status.Value != ClientEndpointStatus.Connected)
                return false;
            base.SendMessage(message);
            return true;
        }
    }

    void SendMessageInternal(Message message)
    {
        lock (_lock)
            base.SendMessage(message);
    }

	// "raw" = not encrypted; callers usually call SendMessage to get optional encryption
    protected override bool SendRawMessage(Message message)
    {
        // NOTE: THIS METHOD MAY RUN ON A BACKGROUND THREAD
        lock (_lock)
        {
            var args = new SocketAsyncEventArgs();
            args.BufferList = message.GetBufferList();
            args.UserToken = message; // just for debugging
            args.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendAttemptEnded);
            if (!_socket.SendAsync(args))
                OnSendAttemptEnded(_socket, args);
            return true;
        }
    }

    /// <summary>
    /// Called when an attempt to send a message to the server has ended, either successfully or
    /// unsuccessfully.
    /// </summary>
    ///
    /// <param name="sender">The event sender.</param>
    ///
    /// <param name="e">Event arguments.</param>
    ///
    void OnSendAttemptEnded(object sender, SocketAsyncEventArgs e)
    {
        // NOTE: THIS METHOD MAY RUN ON A BACKGROUND THREAD
        lock (_lock)
        {
            if (e.SocketError != SocketError.Success)
            {
                CloseConnection(ClientEndpointStatus.LostConnection);
                return;
            }

            var message = (Message)e.UserToken;
        }
    }

    /// <summary>
    /// Called by the base class when <r>ClientEndpoint</r> has successfully connected to the
    /// server, including password verification.  However, the session has not been established yet.
    /// </summary>
    ///
    protected override void OnConnected()
    {
        // NOTE: THIS METHOD RUNS ON A BACKGROUND THREAD
        lock (_lock)
        {
            // next step in the handshake with the server: establish the session, which means:
            //   1. tell the server our session ID, and then
            //   2. wait for the first update message from the server
            var builder = new StringBuilder();
            using (XmlWriter writer = Message.CreateMessageElementWriter(builder))
            {
                writer.WriteStartElement("SessionId");
                writer.WriteValue(_sessionId.ToString());
                writer.WriteEndElement();
            }
            /*
            using (XmlWriter writer = Message.CreateMessageElementWriter(builder))
            {
                writer.WriteStartElement("Abc");
                writer.WriteValue(42);
                writer.WriteEndElement();
            }
            */

            SendMessageInternal(new Message("Session", builder.ToString()));

            _status.Value = ClientEndpointStatus.EstablishingSession;
        }
    }

    protected override void EndConnection(Exception ex)
    {
        // NOTE: THIS METHOD RUNS ON A BACKGROUND THREAD
        lock (_lock)
        {
            if (ex is IncorrectPasswordException)
                CloseConnection(ClientEndpointStatus.PasswordIncorrect);
            else
                CloseConnection(ClientEndpointStatus.LostConnection);
            base.EndConnection(null);
        }
    }

    /// <summary>
    /// Called by the base class when a message has been fully received and decrypted, and is
    /// ready for the application to process.
    /// </summary>
    ///
    protected override void OnMessageReceived(Message message)
    {
        // NOTE: THIS METHOD RUNS ON A BACKGROUND THREAD
        lock (_lock)
        {
            // store the message so it can be processed by Poll (on the UI thread)
            _incomingMessages.Enqueue(message);
        }
    }

    /// <summary>
    /// Called periodically on the UI thread to check if events need to be fired. 
    /// </summary>
    ///
    /// <param name="sender">The event sender.</param>
    ///
    /// <param name="e">Event arguments.</param>
    ///
    void Poll(object sender, EventArgs e)
    {
        ClientEndpointStatus? status = null;
        Message[] incomingMessages = null;
        bool establishingConnection = (_status.Value == ClientEndpointStatus.EstablishingSession);
        lock (_lock)
        {
            if (_status.GetAndResetValueChanged())
                status = _status.Value;
            if (_incomingMessages.Count > 0)
            {
                incomingMessages = _incomingMessages.ToArray();
                _incomingMessages.Clear();
            }

            // if CloseConnection was called, we can now stop polling -- the final events will be
            // fired below
            if (_socket == null)
                _pollTimer.Stop();
        }

        //try // don't need this -- this method runs on the UI thread
        //{
        if ((status != null) && (StatusChanged != null))
        {
            // report status if it has changed; special case: always report final status when
            // CloseConnection is called (i.e. <_socket> is set to null), otherwise important
            // status might be missed; e.g.: PasswordIncorrect, then try again with another
            // bad password, and might never see the second PasswordIncorrect
			if ((ReportedStatus != status) || (_socket == null))
			{
				ReportedStatus = status.Value;
				StatusChanged(this, new ClientEndpointStatusChangedEventArgs()
				{
					Status = status.Value
				});
			}
        }

        if (incomingMessages != null)
        {
            foreach (Message message in incomingMessages)
            {
                if (MessageReceived != null)
                {
                    // we received and processed the first message after connecting to the server -- the
                    // session is now established
                    if (establishingConnection)
                    {
                        lock (_lock)
                        {
                            _status.Value = ClientEndpointStatus.Connected;
                            _status.GetAndResetValueChanged();
                            status = _status.Value;
                        }
                        if (StatusChanged != null)
                        {
                            StatusChanged(this, new ClientEndpointStatusChangedEventArgs()
                            {
                                Status = status.Value
                            });
                        }
                        ReportedStatus = ClientEndpointStatus.Connected;
                    }

                    // process the message
                    MessageReceived(this, new MessageReceivedEventArgs()
                    {
                        Message = message
                    });
                }

            }
        }
        //}
        //catch (Exception ex)
        //{
        //    // see @NOTE in this file
        //    Ignore(ex);
        //    Debugger.Break();
        //    throw; // terminate this thread for security reasons
        //}

        // ping the server periodically to ensure the connection to the server is working -- if
        // it's not, SendMessage will cause the connection to be closed
        if (IsConnected)
        {
            var now = DateTime.UtcNow;
            if ((now - _lastPingTime).TotalMilliseconds >= 1000)
            {
                _lastPingTime = now;
                SendMessage(new Message("Test")); // okay if returns false
            }
        }
    }

    public void CloseConnection()
	{
		// no lock needed -- CloseConnection will lock (_lock)
		CloseConnection(ClientEndpointStatus.ClosedByApplication);
	}

    void CloseConnection(ClientEndpointStatus status)
    {
        lock (_lock)
        {
            // do nothing if we're already closed
            if (_socket == null)
				return;

			// close the connection
            _socket.Close();
            _socket = null;
            _status.Value = status;
            // don't stop <_pollTimer> until it's had a chance to fire Disconnected event

            // clean up the base class state
            EndConnection(null);
        }
    }

    public void Dispose()
    {
        lock (_lock)
        {
            if (IsDisposed)
                return;
            //var messageEndpoint = (MessageEndpoint)this;
            //((IDisposable)messageEndpoint).Dispose();
            CloseConnection();
        }
    }

    /// <summary>
    /// Returns <n>true</n> if a given <r>ClientEndpointStatus</r> value indicates that the
    /// connection is closed.
    /// </summary>
    ///
    /// <param name="status">The given <r>ClientEndpointStatus</r> value.</param>
    ///
    static bool IsConnectionClosedStatus(ClientEndpointStatus status)
    {
    	return (status == ClientEndpointStatus.FailedToConnect) ||
    		   (status == ClientEndpointStatus.PasswordIncorrect) ||
    		   (status == ClientEndpointStatus.LostConnection) ||
    		   (status == ClientEndpointStatus.ClosedByApplication);
    }

	/// <summary>
	/// Ignores arguments.  Suppresses "variable is not used" warnings.
	/// </summary>
	static void Ignore(params object[] args)
	{
	}
}

[DebuggerDisplay("{DebuggerDisplay}")]
public class Status<T>
{
    T _value;
    int _valueChanged; // 0 = false, 1 = true

    public T Value
    {
        get
        {
            return _value;
        }
        set
        {
            _value = value;
            _valueChanged = 1;
        }
    }

    // thread-safe!
    public bool GetAndResetValueChanged()
    {
        return System.Threading.Interlocked.Exchange(ref _valueChanged, 0) != 0;
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Properties
    //

    /// <summary>
    /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
    /// from displaying it in quotes.
    /// </summary>
    object DebuggerDisplay
    {
        get
        {
            return new EncapsulatedString("{0}{1} Value={2}",
                typeof(T).Name, ((_valueChanged != 0) ? " [Changed]" : String.Empty), _value);
        }
    }
}

[DebuggerDisplay("ClientEndpointStatusChangedEventArgs: Status={Status}")]
public class ClientEndpointStatusChangedEventArgs : EventArgs
{
    public ClientEndpointStatus Status { get; internal set; }
}

public enum ClientEndpointStatus
{
    /// <summary>
    /// The <r>ClientEndpoint</r> was constructed but <s>ClientEndpoint.ConnectToServer</s> was
    /// not yet called.
    /// </summary>
    Initialized,

    /// <summary>
    /// <r>ClientEndpoint.ConnectToServer</r> was called, and the connection attempt is underway.
    /// </summary>
    Connecting,

    /// <summary>
    /// <r>ClientEndpoint</r> is performing handshake operations needed to set up the connection.
    /// </summary>
    Negotiating,

    /// <summary>
    /// <r>ClientEndpoint</r> is establishing a session with the server.
    /// </summary>
    EstablishingSession,

    /// <summary>
    /// <r>ClientEndpoint</r> is currently connected to the server.
    /// </summary>
    Connected,

    /// <summary>
    /// The attempt to connect to the server failed at the socket level.  For example, the server
    /// was not found, or the port is not accessible, or the server application is not running.
    /// </summary>
    FailedToConnect,

    /// <summary>
    /// The password is incorrect and so the connection was dropped.
    /// </summary>
    PasswordIncorrect,

    /// <summary>
    /// The connection was lost, after initially succeeding.
    /// </summary>
    LostConnection,

    /// <summary>
    /// the application closed the connection.
    /// </summary>
    ClosedByApplication,
}

}

